/*
 *  Copyright 2010 Applied Card Technologies Ltd
 *  What : Class ComponentAnnotationProcessor
 *  Who  : Farouk Alhassan
 *  When : Friday, February 19, 2010
 */
package com.farent.solutions.plugins.stripes.hyperstripes.core;

import com.farent.solutions.plugins.stripes.hyperstripes.renders.dojo.DojoModelProvider;
import com.farent.solutions.plugins.stripes.hyperstripes.renders.dojo.DojoTagHandler;
import com.farent.solutions.plugins.stripes.hyperstripes.utils.LRUCache;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/**
 * Singleton class for caching annotations and classes that posses them for
 * referencing at runtime.
 * 
 * @author farouka
 */
public class ComponentAnnotationProcessor
{
    private ComponentAnnotationProcessor(){
        //Do Nothing
    }

    /** Regular expression that matches a Java identifier. */
    private static final Pattern JAVA_IDENTIFIER_PATTERN = Pattern.compile ( "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*" );

    protected  static final String ROOT_PKG = "com.farent.solutions";

    protected  static final String CLASS_EXT = ".class";

	private static final Logger logger = Logger.getLogger(
        ComponentAnnotationProcessor.class.getName() );
    
    private static Set<Class> annotations = new HashSet<Class>();

    private static Map<Class, List<Class>> classAnnotationsMap = new Hashtable<Class, List<Class>> ();

    private static ComponentAnnotationProcessor instance;

    /**
     * Keep a cache of only 12 annotations at any instantaneous time T!
     */
    private static final int CACHE_SIZE = 12;
    
    /**
     * A Simple Least reusable cache that maintains only 12 entries. This is to reduce the lookup
     * time for looking up the classes for an annotation.
     *
     * usage: if a request is made for the classes of an annotation then the cache should be checked to
     * see if it has been looked.(a cache-hit) else cache-it once found. This will reduce the lookup time hopefully
     *
     * Also, the Least used annotation will drop off when the limit is reached
     */
    private LRUCache<Class, List<Class>>  annotationCache = new LRUCache<Class, List<Class>> ( CACHE_SIZE );
    
    protected void loadAnnotations ( String pkgName )
    {
        //Get ALl observers
        List classList = new ArrayList ();
//        classList.addAll ( getClassPropertyList ( clazz, pkgName ) );
        classList.addAll ( getClassPropertyList ( ComponentTagHandler.class, pkgName ) );
        classList.addAll ( getClassPropertyList ( ComponentModel.class, pkgName ) );
//        classList.addAll ( getClassPropertyList ( LibraryChangeListener.class, pkgName ) );

        List<Class> all =  Annotations.getAll();
        for ( int i = 0; i < all.size(); i ++ )
        {
            Class clazz = all.get( i );
            classList.addAll ( getClassPropertyList ( clazz, pkgName ) );
        }

        for ( int i = 0; i < classList.size (); i++ )
        {
            //Get each class
            Class topClass = ( Class ) classList.get ( i );

            //Add the annotations on this class to the classAnnotationsMap

            //Find Annotations on class
            List<Class> annotsOnClass =  _annosPresent( topClass );

            //Store Class with List of annotations in map
            if( annotsOnClass != null && annotsOnClass.size () > 0){
                classAnnotationsMap.put ( topClass, annotsOnClass );
            }

            //get nested inner classes
            Class[] innerClasses = topClass.getDeclaredClasses ();
            for ( int j = 0; j < innerClasses.length; j++ )
            {
                Class innerClass = innerClasses[j];
                //Find Annotations on class
                List<Class> annotsOnInnerClass =  _annosPresent( topClass );

                //Store Class with List of annotations in map
                if( annotsOnInnerClass != null && annotsOnInnerClass.size () > 0){
                    //If annotations have already been added to this class
                    if( classAnnotationsMap.get ( innerClass ) != null ){
                        classAnnotationsMap.get ( innerClass ).addAll ( annotsOnInnerClass );
                    } else {
                        classAnnotationsMap.put ( innerClass, annotsOnInnerClass );
                    }
                }
                //Get all Methods on class
                Method[] innerClassMethods = innerClass.getMethods ();
                for ( int k = 0; k < innerClassMethods.length; k++ )
                {
                    Method innerMethod = innerClassMethods[k];
                    //Find Annotations on Methods
                    List<Class> annotsOnInnerMethod =  _annosPresentOnMethod( innerMethod );
                    if( annotsOnInnerMethod != null && annotsOnInnerMethod.size () > 0){
                        //If annotations have already been added to this class
                        if( classAnnotationsMap.get ( innerClass ) != null ){
                            classAnnotationsMap.get ( innerClass ).addAll ( annotsOnInnerMethod );
                        } else {
                            classAnnotationsMap.put ( innerClass, annotsOnInnerMethod );
                        }
                    }
                }
            }

            //Get all Methods on class
            Method[] classMethods = topClass.getMethods ();
            for ( int j = 0; j < classMethods.length; j++ )
            {
                Method method = classMethods[j];
                //Find Annotations on Methods
                List<Class> annotsOnMethod =  _annosPresentOnMethod( method );
                if( annotsOnMethod != null && annotsOnMethod.size () > 0){
                    //If annotations have already been added to this class
                    if( classAnnotationsMap.get ( topClass ) != null ){
                        classAnnotationsMap.get ( topClass ).addAll ( annotsOnMethod );
                    } else {
                        classAnnotationsMap.put ( topClass, annotsOnMethod );
                    }
                }
            }

            //Get all Feilds on class
            Field[] classFeilds = topClass.getDeclaredFields ();
            for ( int j = 0; j < classFeilds.length; j++ )
            {
                Field field = classFeilds[j];
                //Find Annotations on Feild
                List<Class> annotsOnMethod =  _annosPresentOnFeild( field );
                if( annotsOnMethod != null && annotsOnMethod.size () > 0){
                    //If annotations have already been added to this class
                    if( classAnnotationsMap.get ( topClass ) != null ){
                        classAnnotationsMap.get ( topClass ).addAll ( annotsOnMethod );
                    } else {
                        classAnnotationsMap.put ( topClass, annotsOnMethod );
                    }
                }
            }
        }
    }

    /**
     * This is an internal inspection method for looking up annotations on a class
     * Must not be used on out of here
     *
     * @param clz
     * @return
     */
    private List _annosPresent( Class clz ){
        List<Class> classAnnos = new ArrayList<Class> ();

        for ( Iterator<Class> it = annotations.iterator (); it.hasNext (); )
        {
            Class anno = it.next ();
            if( clz.isAnnotationPresent ( anno )){
                classAnnos.add ( anno );
            }
        }
        return classAnnos;
    }


    private List<Class> _annosPresentOnMethod ( Method method )
    {
        List<Class> annosOnMethod = new ArrayList<Class> ();
        for ( Iterator<Class> it = annotations.iterator (); it.hasNext (); )
        {
            Class anno = it.next ();
            if ( method.isAnnotationPresent ( anno ) )
            {
                annosOnMethod.add ( anno );
            }
        }

        return annosOnMethod;
    }

    private List<Class> _annosPresentOnFeild ( Field field )
    {
        List<Class> annosOnField = new ArrayList<Class> ();
        for ( Iterator<Class> it = annotations.iterator (); it.hasNext (); )
        {
            Class anno = it.next ();
            if ( field.isAnnotationPresent ( anno ) )
            {
                annosOnField.add ( anno );
            }
        }

        return annosOnField;
    }

    /**
     * Attempts to find Rosources by auto-discovery in packages specified
     * by pkgName
     *
     * @param targetType the type that we're looking for
     * @return a List of classes found
     */
    protected static <T> List<Class<? extends T>> getClassPropertyList (
                                            Class<T> targetType, String pkgName )
    {
        ClassResolver<T> resolver = new ClassResolver<T> (CLASS_EXT, JAVA_IDENTIFIER_PATTERN );
        String[] packages = ClassResolver.standardSplit ( pkgName );
        resolver.findImplementations ( targetType, packages );
        Set<Class<? extends T>> classes = resolver.getClasses ();
        removeAbstractClasses ( classes );
        return new ArrayList<Class<? extends T>> ( classes );
    }

    /**
     * Removes any classes from the collection that are abstract or interfaces.
     * 
     */
    protected static <T> void removeAbstractClasses ( Collection<Class<? extends T>> classes )
    {
        Iterator<Class<? extends T>> iterator = classes.iterator ();
        while ( iterator.hasNext () )
        {
            Class<? extends T> clazz = iterator.next ();
            if ( clazz.isInterface () )
            {
                iterator.remove ();
            } else if ( ( clazz.getModifiers () & Modifier.ABSTRACT ) == Modifier.ABSTRACT )
            {
                iterator.remove ();
            }
        }
    }

    /**
     * Central method for resistering annotations
     * This implemenation uses a set so no annotation can be registered
     * twice or null values dont slip in
     * 
     * @param cl annotated class
     * @return whether the addition was successful {@link java.util.Set}
     */
    public boolean addAnnotation ( Class cl )
    {
        return annotations.add ( cl );
    }

    /**
     * Startup class called at startup of the application to initialize the
     * ComponentAnnotationProcessor which will then scan for annotations and create
     * a local cache of them for future use
     */
    public void startUp( ){
        initialize ();
        loadAnnotations ( ROOT_PKG );
    }

    /**
     * Initialization method for adding registered annotation classes
     *
     * a local cache of them for future use
     */
    public void initialize(){
        List<Class> annoList = Annotations.getAll ();
        for ( int i = 0; i < annoList.size (); i++ )
        {
            Class class1 = annoList.get ( i );
            addAnnotation ( class1 );
        }
    }

    public Set<Class> getAnnotations ()
    {
        return annotations;
    }

    /**
     * Retreive all the classes that have
     * this annotation on any of their members
     * 
     * @param annotation the annotation to look for
     * @return classes with this annotation on members
     */
    public List<Class> getClassesWithAnnotation ( Class annotation )
    {
        //Check the cache first. 
        List<Class> foundClasses = annotationCache.get ( annotation );
        if ( foundClasses != null )
        {
            return foundClasses;
        } else
        {
            foundClasses = new ArrayList<Class> ();
            Set keyset = classAnnotationsMap.keySet ();
            for ( Iterator it = keyset.iterator (); it.hasNext (); )
            {
                Class classWithAnno = ( Class ) it.next ();
                List annos = classAnnotationsMap.get ( classWithAnno );
                for ( int i = 0; i < annos.size (); i++ )
                {
                    Class foundAnno = ( Class ) annos.get ( i );
                    if ( foundAnno.getName ().equals ( annotation.getName () ) )
                    {
                        foundClasses.add ( classWithAnno );
                    }
                }
            }
            
            //just before you go, store it in the cache.
            annotationCache.put ( annotation, foundClasses );
        }
        return foundClasses;
    }

    /**
     * Method for retreiving all the annotations  for the specified
     * class
     * @param clazz class for whose annotations are searched for
     * @return
     */
    public List<Class> getAnnotationsForClass ( Class clazz )
    {
        List clz = classAnnotationsMap.get ( clazz );
        return clz;
    }

    /**
     * Retreives the single instance of this class.
     * 
     * @return  current instance of this
     */
    public static ComponentAnnotationProcessor getInstance ()
    {
        if(instance == null ) instance = new ComponentAnnotationProcessor ();
        return instance;
    }

    public enum Annotations
    {
        TagHandler ( TagHandler.class ),

        ModelProvider ( ModelProvider.class ),
        DojoModelProvider ( DojoModelProvider.class ),
        DojoTagHandler ( DojoTagHandler.class );
        
        private final Class clazz;

        private Annotations ( Class clz )
        {
            clazz = clz;
        }

        public static List<Class> getAll(){
            List<Class> clazzes = new ArrayList<Class> ();
            for ( Annotations annotations : Annotations.values ())
            {
                clazzes.add ( annotations.clazz );
            }
            return clazzes;
        }
    }

//    public class LRUCache<K, V>
//    {
//
//        private static final float hashTableLoadFactor = 0.75f;
//        private LinkedHashMap<K, V> map;
//        private int cacheSize;
//
//        /**
//         * Creates a new LRU cache.
//         * @param cacheSize the maximum number of entries that will be kept in this cache.
//         */
//        public LRUCache ( int cacheSize )
//        {
//            this.cacheSize = cacheSize;
//            int hashTableCapacity = ( int ) Math.ceil ( cacheSize / hashTableLoadFactor ) + 1;
//            map = new LinkedHashMap<K, V> ( hashTableCapacity, hashTableLoadFactor, true )
//            {
//                // (an anonymous inner class)
//
//                private static final long serialVersionUID = 1;
//
//                @Override
//                protected boolean removeEldestEntry ( Map.Entry<K, V> eldest )
//                {
//                    return size () > LRUCache.this.cacheSize;
//                }
//            };
//        }
//
//        /**
//         * Retrieves an entry from the cache.<br>
//         * The retrieved entry becomes the MRU (most recently used) entry.
//         * @param key the key whose associated value is to be returned.
//         * @return    the value associated to this key, or null if no value with this key exists in the cache.
//         */
//        public synchronized V get ( K key )
//        {
//            return map.get ( key );
//        }
//
//        /**
//         * Adds an entry to this cache.
//         * If the cache is full, the LRU (least recently used) entry is dropped.
//         * @param key    the key with which the specified value is to be associated.
//         * @param value  a value to be associated with the specified key.
//         */
//        public synchronized void put ( K key, V value )
//        {
//            map.put ( key, value );
//        }
//
//        /**
//         * Clears the cache.
//         */
//        public synchronized void clear ()
//        {
//            map.clear ();
//        }
//
//        /**
//         * Returns the number of used entries in the cache.
//         * @return the number of entries currently in the cache.
//         */
//        public synchronized int usedEntries ()
//        {
//            return map.size ();
//        }
//
//        /**
//         * Returns a <code>Collection</code> that contains a copy of all cache entries.
//         * @return a <code>Collection</code> with a copy of the cache content.
//         */
//        public synchronized Collection<Map.Entry<K, V>> getAll ()
//        {
//            return new ArrayList<Map.Entry<K, V>> ( map.entrySet () );
//        }
//    } // end class LRUCache

}
